--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit dd8bfdde67287b656075624fee2542c60c73a620
Parents : e91264d
Author : Ivan <e46112d44649266d71fe2193e00a4710>
Signature : T66BB85Valid, signed by author
Date : 2026-07-26T10:28:14-05:00
chore: various fixes and updates
Changes
23 files changed, 693 insertions(+), 94 deletions(-)
Diff
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8993d909..27fe9042 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,8 +18,9 @@ All notable changes to this project will be documented in this file.
- **Android RNode flasher**: Open native flasher returns a real status, keeps USB-serial classes through R8, uses an ActionBar theme, and surfaces startup failures instead of silently doing nothing. Bluetooth Open settings tries GrapheneOS-friendly fallbacks (app details, Bluetooth settings, general Settings) instead of toasting unavailable.
- **Connection banners**: Do not flash disconnected on startup before the first successful WebSocket open. Debounce disconnect UI for 2.5s and only show reconnected when the disconnect banner was actually shown. Foreground recovery prefers a ping for longer before forcing a reconnect.
- **Android calls**: Clarify that the web audio bridge on Android uses native mic and speaker through the telephone audio bridge, not browser getUserMedia.
-- **Browser calls (Docker / HTTPS)**: Refresh Devices calls getUserMedia first so Brave and Chromium show the microphone permission prompt instead of failing early when enumerateDevices lists no inputs before permission is granted.
-- **HTTP security headers**: Send Permissions-Policy allowing microphone and camera for this origin so reverse proxies that omit the header do not block capture by default.
+- **Browser calls (Docker / HTTPS)**: Refresh Devices prompts with bare `getUserMedia({ audio: true })` first so Brave and Chromium show the microphone dialog. Constrained requests (echoCancellation and friends) often return NotFoundError before permission is granted and never ask. Clearer toasts for insecure HTTP and pending/denied mic permission.
+- **HTTP security headers**: Send Permissions-Policy allowing microphone, camera, bluetooth, serial, and usb for this origin so reverse proxies that omit the header do not block capture or RNode flasher hardware APIs by default.
+- **RNode flasher Bluetooth**: Detect Brave's disabled-by-default Web Bluetooth API, show how to enable `brave://flags/#brave-web-bluetooth-api`, and offer Try Bluetooth / Recheck actions. Web Bluetooth has no mic-style prompt. The device chooser from `requestDevice()` is the permission UI.
- **UI language**: Persist language changes over the config HTTP API (not WebSocket-only), normalize legacy locale codes, and stop the Reticulum manual language picker from overwriting app UI language.
- **Network visualizer**: WebGL background follows light theme and clears while the WASM scene is still loading. Boot theme removes stale dark class when light is selected.
- **Translator (Landlock)**: On Linux, allow read/execute for user-local pipx CLIs (`~/.local/bin`, `~/.local/share/pipx`) and read-write for Argos Translate data under `~/.local/share/argos-translate`, so `argospm` language lists and local Argos translation work with the filesystem sandbox enabled.
diff --git a/meshchatx.rsm b/meshchatx.rsm
index ffca7bf3..12295d16 100644
Binary files a/meshchatx.rsm and b/meshchatx.rsm differ
diff --git a/meshchatx/src/backend/http/middleware.py b/meshchatx/src/backend/http/middleware.py
index 2c8224a3..63ed1bee 100644
--- a/meshchatx/src/backend/http/middleware.py
+++ b/meshchatx/src/backend/http/middleware.py
@@ -281,9 +281,12 @@ def create_security_middleware(app):
response.headers["X-XSS-Protection"] = "1; mode=block"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
- # Explicitly allow mic/camera for this origin. Reverse proxies that omit
- # Permissions-Policy are fine. This documents intent for Brave and Chromium.
- response.headers["Permissions-Policy"] = "microphone=(self), camera=(self)"
+ # Explicitly allow mic/camera and hardware transports for this origin.
+ # Listing only mic/camera without bluetooth/serial/usb has caused some
+ # Chromium and Brave builds to treat hardware APIs as unavailable.
+ response.headers["Permissions-Policy"] = (
+ "microphone=(self), camera=(self), bluetooth=(self), serial=(self), usb=(self)"
+ )
# CSP base configuration
privacy_mode = privacy_mode_enabled(app.config)
diff --git a/meshchatx/src/frontend/components/call/CallPage.vue b/meshchatx/src/frontend/components/call/CallPage.vue
index 7d6e4d7e..868f0d8d 100644
--- a/meshchatx/src/frontend/components/call/CallPage.vue
+++ b/meshchatx/src/frontend/components/call/CallPage.vue
@@ -2306,7 +2306,9 @@ export default {
: new Set();
const sid = this.selectedAudioInputId;
if (sid === "__meshchat_default_in__") {
- return { audio: processingHints };
+ // Bare audio first for Default: processing flags alone can yield
+ // NotFoundError on Brave and Chromium before mic permission is granted.
+ return { audio: true };
}
const id = sid && validIds.has(sid) ? sid : null;
return id ? { audio: { ...processingHints, deviceId: { exact: id } } } : { audio: processingHints };
@@ -2707,37 +2709,74 @@ export default {
return true;
}
}
+ if (typeof window !== "undefined" && window.isSecureContext === false) {
+ ToastUtils.error(this.$t("call.microphone_insecure_context"));
+ return false;
+ }
const mediaDevices = this.getMediaDevicesApi();
if (!mediaDevices) {
throw new Error("navigator.mediaDevices is unavailable");
}
- // Do not gate on enumerateDevices before getUserMedia. Brave and
- // Chromium often omit audioinput (or only list speakers) until the
- // mic permission prompt has been accepted. Calling getUserMedia
- // first is what shows the browser permission dialog.
+ // Brave and Chromium often throw NotFoundError for constrained
+ // getUserMedia (echoCancellation and friends) before permission is
+ // granted, and never show a prompt. Wide-open { audio: true } is what
+ // reliably opens the browser microphone permission dialog.
this.selectedAudioInputId = "__meshchat_default_in__";
- const stream = await mediaDevices.getUserMedia({
- audio: {
- echoCancellation: true,
- noiseSuppression: true,
- autoGainControl: true,
- },
- });
+ let stream;
+ try {
+ stream = await mediaDevices.getUserMedia({ audio: true });
+ } catch (firstErr) {
+ const retryable =
+ firstErr?.name === "NotFoundError" ||
+ firstErr?.name === "OverconstrainedError" ||
+ firstErr?.name === "NotReadableError";
+ if (!retryable) {
+ throw firstErr;
+ }
+ this.logWebAudioFailure("request-permission-retry-processing", firstErr);
+ // Some hosts accept processing hints after a failed bare probe.
+ stream = await mediaDevices.getUserMedia({
+ audio: {
+ echoCancellation: true,
+ noiseSuppression: true,
+ autoGainControl: true,
+ },
+ });
+ }
stream.getTracks().forEach((t) => t.stop());
await this.refreshAudioDevices();
return true;
} catch (e) {
this.logWebAudioFailure("request-permission", e);
- const errorKey =
- e?.name === "NotFoundError" || e?.name === "OverconstrainedError"
- ? "call.no_audio_input_found"
- : e?.name === "NotAllowedError"
- ? "call.microphone_permission_denied"
- : "call.web_audio_not_available";
+ let errorKey = "call.web_audio_not_available";
+ if (e?.name === "NotAllowedError" || e?.name === "SecurityError") {
+ errorKey = "call.microphone_permission_denied";
+ } else if (e?.name === "NotFoundError" || e?.name === "OverconstrainedError") {
+ errorKey = await this.resolveMissingMicErrorKey();
+ }
ToastUtils.error(this.$t(errorKey));
return false;
}
},
+ async resolveMissingMicErrorKey() {
+ // Chromium sometimes reports NotFoundError when permission is blocked
+ // or when the permission prompt never appeared. Prefer a clearer toast.
+ try {
+ const perms = navigator?.permissions;
+ if (perms && typeof perms.query === "function") {
+ const status = await perms.query({ name: "microphone" });
+ if (status?.state === "denied") {
+ return "call.microphone_permission_denied";
+ }
+ if (status?.state === "prompt") {
+ return "call.microphone_permission_needed";
+ }
+ }
+ } catch {
+ // Permissions API name may be unsupported.
+ }
+ return "call.no_audio_input_found";
+ },
async refreshAudioDevices() {
const defaultIn = {
deviceId: "__meshchat_default_in__",
diff --git a/meshchatx/src/frontend/components/rnode/RNodeCapabilitiesBanner.vue b/meshchatx/src/frontend/components/rnode/RNodeCapabilitiesBanner.vue
index a2c9dec5..f0fc1dc3 100644
--- a/meshchatx/src/frontend/components/rnode/RNodeCapabilitiesBanner.vue
+++ b/meshchatx/src/frontend/components/rnode/RNodeCapabilitiesBanner.vue
@@ -103,35 +103,48 @@ export default {
},
_bluetoothActions() {
const actions = [];
- if (!this.androidAvailable) {
- return actions;
- }
const bluetooth = this.capabilities?.transports?.[TRANSPORT_BLUETOOTH];
- const needsPermission = bluetooth?.reason === "android_bluetooth_permission_required";
- if (needsPermission) {
- actions.push({
- id: "request-bluetooth",
- icon: "bluetooth-settings",
- labelKey: "tools.rnode_flasher.support.actions.request_bluetooth",
- });
- actions.push({
- id: "open-bluetooth-settings",
- icon: "cog",
- labelKey: "tools.rnode_flasher.support.actions.open_settings",
- });
- } else {
- // Permissions granted (or N/A). WebView still cannot flash over BLE.
- actions.push({
- id: "open-native-flasher",
- icon: "usb",
- labelKey: "tools.rnode_flasher.support.actions.open_native",
- });
- actions.push({
- id: "open-bluetooth-settings",
- icon: "cog",
- labelKey: "tools.rnode_flasher.support.actions.open_settings",
- });
+ if (this.androidAvailable) {
+ const needsPermission = bluetooth?.reason === "android_bluetooth_permission_required";
+ if (needsPermission) {
+ actions.push({
+ id: "request-bluetooth",
+ icon: "bluetooth-settings",
+ labelKey: "tools.rnode_flasher.support.actions.request_bluetooth",
+ });
+ actions.push({
+ id: "open-bluetooth-settings",
+ icon: "cog",
+ labelKey: "tools.rnode_flasher.support.actions.open_settings",
+ });
+ } else {
+ // Permissions granted (or N/A). WebView still cannot flash over BLE.
+ actions.push({
+ id: "open-native-flasher",
+ icon: "usb",
+ labelKey: "tools.rnode_flasher.support.actions.open_native",
+ });
+ actions.push({
+ id: "open-bluetooth-settings",
+ icon: "cog",
+ labelKey: "tools.rnode_flasher.support.actions.open_settings",
+ });
+ }
+ return actions;
}
+ // Desktop browsers: Web Bluetooth has no ambient permission grant.
+ // The chooser from requestDevice() is the permission UI. Offer a probe
+ // that either opens it or tells the user how to enable the API (Brave).
+ actions.push({
+ id: "probe-bluetooth",
+ icon: "bluetooth-connect",
+ labelKey: "tools.rnode_flasher.support.actions.probe_bluetooth",
+ });
+ actions.push({
+ id: "recheck-capabilities",
+ icon: "refresh",
+ labelKey: "tools.rnode_flasher.support.actions.recheck_capabilities",
+ });
return actions;
},
},
diff --git a/meshchatx/src/frontend/components/tools/RNodeFlasherPage.vue b/meshchatx/src/frontend/components/tools/RNodeFlasherPage.vue
index de9970c7..df5e3f28 100644
--- a/meshchatx/src/frontend/components/tools/RNodeFlasherPage.vue
+++ b/meshchatx/src/frontend/components/tools/RNodeFlasherPage.vue
@@ -330,6 +330,53 @@ export default {
}
return;
}
+ if (action === "recheck-capabilities") {
+ this.refreshCapabilities();
+ const bt = this.capabilities?.transports?.bluetooth;
+ if (bt?.available) {
+ ToastUtils.success(this.$t("tools.rnode_flasher.support.actions.bluetooth_now_available"));
+ } else {
+ ToastUtils.info(this.$t("tools.rnode_flasher.support.actions.bluetooth_still_unavailable"));
+ }
+ return;
+ }
+ if (action === "probe-bluetooth") {
+ await this.probeWebBluetooth();
+ }
+ },
+ async probeWebBluetooth() {
+ this.refreshCapabilities();
+ if (typeof window !== "undefined" && window.isSecureContext === false) {
+ ToastUtils.error(this.$t("tools.rnode_flasher.support.bluetooth.insecure_context"));
+ return;
+ }
+ if (!navigator?.bluetooth) {
+ const reason = this.capabilities?.transports?.bluetooth?.reason;
+ if (reason === "brave_flag_disabled") {
+ ToastUtils.warning(this.$t("tools.rnode_flasher.support.bluetooth.brave_enable_flag"));
+ } else {
+ ToastUtils.warning(this.$t("tools.rnode_flasher.support.bluetooth.browser_unsupported"));
+ }
+ return;
+ }
+ // requestDevice() is the browser permission / chooser UI. There is no
+ // separate ambient "allow bluetooth" prompt like microphone.
+ try {
+ await BluetoothTransport.request();
+ this.connectionMethod = TRANSPORT_BLUETOOTH;
+ this.refreshCapabilities();
+ ToastUtils.success(this.$t("tools.rnode_flasher.support.actions.bluetooth_probe_ok"));
+ } catch (e) {
+ if (e?.code === "NO_DEVICE_SELECTED") {
+ ToastUtils.info(this.$t("tools.rnode_flasher.support.actions.bluetooth_probe_cancelled"));
+ return;
+ }
+ ToastUtils.error(
+ this.$t("tools.rnode_flasher.support.actions.bluetooth_probe_failed", {
+ error: e?.message || String(e),
+ })
+ );
+ }
},
async fetchLatestRelease() {
try {
diff --git a/meshchatx/src/frontend/js/rnode/Capabilities.js b/meshchatx/src/frontend/js/rnode/Capabilities.js
index 9df02736..47f8dafb 100644
--- a/meshchatx/src/frontend/js/rnode/Capabilities.js
+++ b/meshchatx/src/frontend/js/rnode/Capabilities.js
@@ -34,10 +34,12 @@ function detectPlatform(env) {
const isAndroid = ANDROID_RE.test(ua);
const isElectron = ELECTRON_RE.test(ua) || Boolean(env.electron);
const hasMeshChatXAndroid = Boolean(env.MeshChatXAndroid);
+ const isBrave = /Brave/i.test(ua) || Boolean(env.navigator?.brave) || Boolean(env.brave);
return {
isAndroid,
isElectron,
hasMeshChatXAndroid,
+ isBrave,
isSecureContext: Boolean(env.isSecureContext),
userAgent: ua,
};
@@ -109,6 +111,13 @@ function detectBluetooth(env, platform) {
reason: null,
};
}
+ if (!platform.isSecureContext) {
+ return {
+ available: false,
+ kind: "none",
+ reason: "insecure_context",
+ };
+ }
if (platform.hasMeshChatXAndroid) {
const bridge = env.MeshChatXAndroid;
const hasPerms =
@@ -122,10 +131,19 @@ function detectBluetooth(env, platform) {
reason: hasPerms ? "android_bridge_no_web_bluetooth" : "android_bluetooth_permission_required",
};
}
+ // Brave ships Chromium but disables Web Bluetooth until the flag is on.
+ // navigator.bluetooth is missing in that state, which looks like "unsupported".
+ if (platform.isBrave) {
+ return {
+ available: false,
+ kind: "none",
+ reason: "brave_flag_disabled",
+ };
+ }
return {
available: false,
kind: "none",
- reason: platform.isSecureContext ? "browser_unsupported" : "insecure_context",
+ reason: "browser_unsupported",
};
}
@@ -202,6 +220,13 @@ export function transportSuggestionKeys(capabilities, transportName) {
if (transportName === TRANSPORT_BLUETOOTH && !platform.isSecureContext) {
suggestions.push("tools.rnode_flasher.support.bluetooth.requires_https");
}
+ if (transportName === TRANSPORT_BLUETOOTH && reason === "brave_flag_disabled") {
+ suggestions.push("tools.rnode_flasher.support.bluetooth.brave_enable_flag");
+ suggestions.push("tools.rnode_flasher.support.bluetooth.brave_recheck");
+ }
+ if (transportName === TRANSPORT_BLUETOOTH && reason === "browser_unsupported") {
+ suggestions.push("tools.rnode_flasher.support.bluetooth.chromium_linux_hint");
+ }
return suggestions;
}
diff --git a/meshchatx/src/frontend/locales/de.json b/meshchatx/src/frontend/locales/de.json
index 1c763be7..346d9a59 100644
--- a/meshchatx/src/frontend/locales/de.json
+++ b/meshchatx/src/frontend/locales/de.json
@@ -2576,12 +2576,16 @@
"bluetooth": {
"title": "Bluetooth ist nicht verfügbar",
"android_bridge_not_implemented": "Web Bluetooth ist hier nicht verfügbar. Verwenden Sie die Bluetooth-Einstellungen des Systems oder einen anderen Browser.",
- "browser_unsupported": "Dieser Browser unterstützt Web Bluetooth nicht. Versuchen Sie Chrome oder Edge.",
+ "browser_unsupported": "Dieser Browser stellt Web Bluetooth nicht bereit. Verwenden Sie einen Chromium-Build mit aktiviertem Web Bluetooth oder aktivieren Sie die API-Flagge, falls Ihr Browser sie ausblendet.",
"insecure_context": "Web Bluetooth erfordert einen sicheren Kontext (HTTPS oder localhost).",
"requires_https": "Öffnen Sie den Flasher über HTTPS oder localhost, um Bluetooth zu aktivieren.",
"unknown": "Bluetooth kann in dieser Umgebung nicht initialisiert werden.",
"android_bridge_no_web_bluetooth": "Bluetooth-Berechtigung erteilt. Flashen Sie über USB im nativen Flasher (Web Bluetooth ist in der WebView nicht verfügbar).",
- "android_bluetooth_permission_required": "Für Mesh-RNode-BLE ist die Bluetooth-Berechtigung erforderlich. Tippen Sie auf Bluetooth erlauben."
+ "android_bluetooth_permission_required": "Für Mesh-RNode-BLE ist die Bluetooth-Berechtigung erforderlich. Tippen Sie auf Bluetooth erlauben.",
+ "brave_flag_disabled": "Brave deaktiviert Web Bluetooth standardmäßig, daher erscheint nie eine Geräteauswahl.",
+ "brave_enable_flag": "Öffnen Sie in Brave brave://flags/#brave-web-bluetooth-api, setzen Sie Web Bluetooth API auf Enabled, starten Sie neu und tippen Sie dann auf Erneut prüfen.",
+ "brave_recheck": "Nach dem Aktivieren der Flagge Erneut prüfen verwenden. Die Kopplung nutzt die Geräteauswahl von requestDevice (es gibt keinen separaten Berechtigungsdialog wie beim Mikrofon).",
+ "chromium_linux_hint": "Unter Linux Chromium braucht BlueZ und einen sicheren Ursprung (HTTPS oder localhost). Tippen Sie auf Bluetooth testen, um die Geräteauswahl zu öffnen, sobald die API verfügbar ist."
},
"actions": {
"load_polyfill": "Polyfill laden",
@@ -2599,7 +2603,14 @@
"bluetooth_unsupported": "Anfrage der Bluetooth-Berechtigung ist nicht verfügbar.",
"bluetooth_granted": "Bluetooth-Berechtigung erteilt.",
"bluetooth_denied": "Bluetooth-Berechtigung verweigert.",
- "usb_requested": "USB-Berechtigung angefordert."
+ "usb_requested": "USB-Berechtigung angefordert.",
+ "probe_bluetooth": "Bluetooth testen",
+ "recheck_capabilities": "Bluetooth erneut prüfen",
+ "bluetooth_now_available": "Web Bluetooth ist verfügbar. Wählen Sie Bluetooth und fahren Sie fort.",
+ "bluetooth_still_unavailable": "Web Bluetooth ist in diesem Browser weiterhin nicht verfügbar.",
+ "bluetooth_probe_ok": "Bluetooth-Geräteauswahl hat funktioniert. Bluetooth-Transport ist bereit.",
+ "bluetooth_probe_cancelled": "Bluetooth-Geräteauswahl abgebrochen.",
+ "bluetooth_probe_failed": "Bluetooth-Test fehlgeschlagen: {error}"
}
},
"diagnostics": {
@@ -3136,7 +3147,9 @@
"ringtone_saved": "Klingelton erfolgreich gespeichert",
"failed_save_ringtone": "Fehler beim Speichern des bearbeiteten Klingeltons",
"codec2_unavailable": "Codec2 is not available on this device. Low-bandwidth call profiles are hidden.",
- "codec2_profile_remapped": "Codec2 is unavailable so the call profile was switched to Opus."
+ "codec2_profile_remapped": "Codec2 is unavailable so the call profile was switched to Opus.",
+ "microphone_permission_needed": "Erlauben Sie den Mikrofonzugriff, wenn der Browser danach fragt, und klicken Sie danach erneut auf Geräte aktualisieren.",
+ "microphone_insecure_context": "Mikrofonzugriff benötigt HTTPS (oder localhost). Öffnen Sie MeshChatX über eine sichere URL und versuchen Sie es erneut."
},
"tutorial": {
"title": "Erste Schritte",
diff --git a/meshchatx/src/frontend/locales/en.json b/meshchatx/src/frontend/locales/en.json
index 6f82c109..216b397e 100644
--- a/meshchatx/src/frontend/locales/en.json
+++ b/meshchatx/src/frontend/locales/en.json
@@ -2788,7 +2788,11 @@
"android_bridge_not_implemented": "Web Bluetooth is not available in WebView. Use the native flasher for USB, and Allow Bluetooth for mesh RNode BLE.",
"android_bridge_no_web_bluetooth": "Bluetooth permission is granted. Flash over USB in the native flasher (Web Bluetooth is not available in WebView).",
"android_bluetooth_permission_required": "Bluetooth permission is required for mesh RNode BLE. Tap Allow Bluetooth.",
- "browser_unsupported": "This browser does not support Web Bluetooth. Try Chrome or Edge.",
+ "browser_unsupported": "This browser does not expose Web Bluetooth. Use a Chromium build with Web Bluetooth enabled, or enable the API flag if your browser hides it.",
+ "brave_flag_disabled": "Brave disables Web Bluetooth by default, so the browser never shows a device prompt.",
+ "brave_enable_flag": "In Brave open brave://flags/#brave-web-bluetooth-api, set Web Bluetooth API to Enabled, relaunch, then tap Recheck.",
+ "brave_recheck": "After enabling the flag, use Recheck Bluetooth. Pairing uses the device chooser from requestDevice (there is no separate mic-style permission dialog).",
+ "chromium_linux_hint": "On Linux Chromium needs working BlueZ and a secure origin (HTTPS or localhost). Tap Try Bluetooth to open the device chooser when the API is present.",
"insecure_context": "Web Bluetooth requires a secure context (HTTPS or localhost).",
"requires_https": "Open the flasher over HTTPS or via localhost to enable Bluetooth.",
"unknown": "Bluetooth cannot be initialised in this environment."
@@ -2796,6 +2800,8 @@
"actions": {
"load_polyfill": "Load polyfill",
"request_bluetooth": "Allow Bluetooth",
+ "probe_bluetooth": "Try Bluetooth",
+ "recheck_capabilities": "Recheck Bluetooth",
"request_usb": "Allow USB",
"open_native": "Open native flasher",
"open_settings": "Open settings",
@@ -2809,6 +2815,11 @@
"bluetooth_unsupported": "Bluetooth permission request is unavailable.",
"bluetooth_granted": "Bluetooth permission granted.",
"bluetooth_denied": "Bluetooth permission denied.",
+ "bluetooth_now_available": "Web Bluetooth is available. Select Bluetooth and continue.",
+ "bluetooth_still_unavailable": "Web Bluetooth is still unavailable in this browser.",
+ "bluetooth_probe_ok": "Bluetooth device chooser worked. Bluetooth transport is ready.",
+ "bluetooth_probe_cancelled": "Bluetooth device selection cancelled.",
+ "bluetooth_probe_failed": "Bluetooth probe failed: {error}",
"usb_requested": "USB permission requested."
}
},
@@ -3703,6 +3714,8 @@
"web_audio_not_available": "Web audio not available",
"no_audio_input_found": "No input audio device found. Check microphone and permissions.",
"microphone_permission_denied": "Microphone permission denied. Allow access and try again.",
+ "microphone_permission_needed": "Allow microphone access when the browser prompts, then click Refresh Devices again.",
+ "microphone_insecure_context": "Microphone access needs HTTPS (or localhost). Open MeshChatX over a secure URL and try again.",
"failed_to_update_dnd": "Failed to update Do Not Disturb status",
"failed_to_update_call_settings": "Failed to update call settings",
"failed_to_update_recording_status": "Failed to update call recording status",
diff --git a/meshchatx/src/frontend/locales/es.json b/meshchatx/src/frontend/locales/es.json
index a12aced0..29d6adca 100644
--- a/meshchatx/src/frontend/locales/es.json
+++ b/meshchatx/src/frontend/locales/es.json
@@ -2782,12 +2782,16 @@
"bluetooth": {
"title": "Bluetooth no disponible",
"android_bridge_not_implemented": "Web Bluetooth no está disponible aquí. Use la configuración de Bluetooth del sistema o un navegador diferente.",
- "browser_unsupported": "Este navegador no soporta Web Bluetooth. Pruebe con Chrome o Edge.",
+ "browser_unsupported": "Este navegador no expone Web Bluetooth. Use una compilación Chromium con Web Bluetooth activado, o habilite la bandera de la API si el navegador la oculta.",
"insecure_context": "Web Bluetooth requiere un contexto seguro (HTTPS o localhost).",
"requires_https": "Abra el flasher a través de HTTPS o localhost para habilitar Bluetooth.",
"unknown": "Bluetooth no se puede inicializar en este entorno.",
"android_bridge_no_web_bluetooth": "Permiso de Bluetooth concedido. Flashea por USB en el flasher nativo (Web Bluetooth no está disponible en WebView).",
- "android_bluetooth_permission_required": "Se requiere permiso de Bluetooth para RNode BLE de malla. Toca Permitir Bluetooth."
+ "android_bluetooth_permission_required": "Se requiere permiso de Bluetooth para RNode BLE de malla. Toca Permitir Bluetooth.",
+ "brave_flag_disabled": "Brave desactiva Web Bluetooth por defecto, así que el navegador nunca muestra el selector de dispositivos.",
+ "brave_enable_flag": "En Brave abra brave://flags/#brave-web-bluetooth-api, ponga Web Bluetooth API en Enabled, reinicie y pulse Volver a comprobar.",
+ "brave_recheck": "Tras activar la bandera, use Volver a comprobar Bluetooth. El emparejamiento usa el selector de requestDevice (no hay un diálogo de permiso aparte como el del micrófono).",
+ "chromium_linux_hint": "En Linux, Chromium necesita BlueZ y un origen seguro (HTTPS o localhost). Pulse Probar Bluetooth para abrir el selector cuando la API esté presente."
},
"actions": {
"load_polyfill": "Cargar polyfill",
@@ -2805,7 +2809,14 @@
"bluetooth_unsupported": "La solicitud de permiso de Bluetooth no está disponible.",
"bluetooth_granted": "Permiso de Bluetooth concedido.",
"bluetooth_denied": "Permiso de Bluetooth denegado.",
- "usb_requested": "Permiso USB solicitado."
+ "usb_requested": "Permiso USB solicitado.",
+ "probe_bluetooth": "Probar Bluetooth",
+ "recheck_capabilities": "Volver a comprobar Bluetooth",
+ "bluetooth_now_available": "Web Bluetooth está disponible. Seleccione Bluetooth y continúe.",
+ "bluetooth_still_unavailable": "Web Bluetooth sigue no disponible en este navegador.",
+ "bluetooth_probe_ok": "El selector de dispositivos Bluetooth funcionó. El transporte Bluetooth está listo.",
+ "bluetooth_probe_cancelled": "Selección de dispositivo Bluetooth cancelada.",
+ "bluetooth_probe_failed": "La prueba de Bluetooth falló: {error}"
}
},
"diagnostics": {
@@ -3342,7 +3353,9 @@
"ringtone_saved": "Ringtone se salvó con éxito",
"failed_save_ringtone": "Error al guardar el tono editado",
"codec2_unavailable": "Codec2 is not available on this device. Low-bandwidth call profiles are hidden.",
- "codec2_profile_remapped": "Codec2 is unavailable so the call profile was switched to Opus."
+ "codec2_profile_remapped": "Codec2 is unavailable so the call profile was switched to Opus.",
+ "microphone_permission_needed": "Permita el acceso al micrófono cuando el navegador lo solicite y luego pulse Actualizar dispositivos de nuevo.",
+ "microphone_insecure_context": "El acceso al micrófono requiere HTTPS (o localhost). Abra MeshChatX con una URL segura e inténtelo de nuevo."
},
"contacts": {
"title": "Contactos",
diff --git a/meshchatx/src/frontend/locales/fi.json b/meshchatx/src/frontend/locales/fi.json
index 1bdf8b7a..4ead2661 100644
--- a/meshchatx/src/frontend/locales/fi.json
+++ b/meshchatx/src/frontend/locales/fi.json
@@ -2782,12 +2782,16 @@
"bluetooth": {
"title": "Bluetooth ei ole käytettävissä",
"android_bridge_not_implemented": "Web Bluetooth ei ole käytettävissä täällä. Käytä käyttöjärjestelmän Bluetooth-asetuksia tai toista selainta.",
- "browser_unsupported": "Tämä selain ei tue Web Bluetoothia. Kokeile Chromea tai Edgeä.",
+ "browser_unsupported": "Tämä selain ei tarjoa Web Bluetoothia. Käytä Chromium-buildia, jossa Web Bluetooth on käytössä, tai ota API-lippu käyttöön, jos selain piilottaa sen.",
"insecure_context": "Web Bluetooth vaatii suojatun yhteyden (HTTPS tai localhost).",
"requires_https": "Avaa flasher HTTPS:n tai localhostin kautta ottaaksesi Bluetoothin käyttöön.",
"unknown": "Bluetoothia ei voida alustaa tässä ympäristössä.",
"android_bridge_no_web_bluetooth": "Bluetooth-lupa myönnetty. Flashaa USB:llä natiivissa flasherissa (Web Bluetooth ei ole käytettävissä WebViewissä).",
- "android_bluetooth_permission_required": "Mesh-RNode BLE vaatii Bluetooth-luvan. Napauta Salli Bluetooth."
+ "android_bluetooth_permission_required": "Mesh-RNode BLE vaatii Bluetooth-luvan. Napauta Salli Bluetooth.",
+ "brave_flag_disabled": "Brave poistaa Web Bluetoothin oletuksena käytöstä, joten selain ei koskaan näytä laitevalitsinta.",
+ "brave_enable_flag": "Avaa Bravessa brave://flags/#brave-web-bluetooth-api, aseta Web Bluetooth API tilaan Enabled, käynnistä uudelleen ja napauta Tarkista uudelleen.",
+ "brave_recheck": "Lipun käyttöönoton jälkeen käytä Tarkista Bluetooth uudelleen. Paritus käyttää requestDevice-laitteen valitsinta (ei erillistä lupadialogia kuten mikrofonilla).",
+ "chromium_linux_hint": "Linuxissa Chromium tarvitsee toimivan BlueZ:n ja suojatun alkuperän (HTTPS tai localhost). Napauta Kokeile Bluetoothia avataksesi valitsimen, kun API on käytettävissä."
},
"actions": {
"load_polyfill": "Lataa polyfill",
@@ -2805,7 +2809,14 @@
"bluetooth_unsupported": "Bluetooth-lupapyyntö ei ole käytettävissä.",
"bluetooth_granted": "Bluetooth-lupa myönnetty.",
"bluetooth_denied": "Bluetooth-lupa evätty.",
- "usb_requested": "USB-lupaa pyydetty."
+ "usb_requested": "USB-lupaa pyydetty.",
+ "probe_bluetooth": "Kokeile Bluetoothia",
+ "recheck_capabilities": "Tarkista Bluetooth uudelleen",
+ "bluetooth_now_available": "Web Bluetooth on käytettävissä. Valitse Bluetooth ja jatka.",
+ "bluetooth_still_unavailable": "Web Bluetooth ei ole edelleenkään käytettävissä tässä selaimessa.",
+ "bluetooth_probe_ok": "Bluetooth-laitteen valitsin toimi. Bluetooth-siirtoyhteys on valmis.",
+ "bluetooth_probe_cancelled": "Bluetooth-laitteen valinta peruutettiin.",
+ "bluetooth_probe_failed": "Bluetooth-testi epäonnistui: {error}"
}
},
"diagnostics": {
@@ -3549,7 +3560,9 @@
"ringtone_saved": "Soittoääni tallennettu onnistuneesti",
"failed_save_ringtone": "Muokatun soittoäänen tallentaminen epäonnistui",
"codec2_unavailable": "Codec2 is not available on this device. Low-bandwidth call profiles are hidden.",
- "codec2_profile_remapped": "Codec2 is unavailable so the call profile was switched to Opus."
+ "codec2_profile_remapped": "Codec2 is unavailable so the call profile was switched to Opus.",
+ "microphone_permission_needed": "Salli mikrofonin käyttö, kun selain pyytää sitä, ja napsauta sitten Päivitä laitteet uudelleen.",
+ "microphone_insecure_context": "Mikrofonin käyttö edellyttää HTTPS:ää (tai localhostia). Avaa MeshChatX suojatulla URL-osoitteella ja yritä uudelleen."
},
"contacts": {
"title": "Yhteystiedot",
diff --git a/meshchatx/src/frontend/locales/fr.json b/meshchatx/src/frontend/locales/fr.json
index 5a2512fb..aef6369b 100644
--- a/meshchatx/src/frontend/locales/fr.json
+++ b/meshchatx/src/frontend/locales/fr.json
@@ -2782,12 +2782,16 @@
"bluetooth": {
"title": "Bluetooth indisponible",
"android_bridge_not_implemented": "Web Bluetooth n'est pas disponible ici. Utilisez les paramètres Bluetooth du système ou un autre navigateur.",
- "browser_unsupported": "Ce navigateur ne prend pas en charge Web Bluetooth. Essayez Chrome ou Edge.",
+ "browser_unsupported": "Ce navigateur n'expose pas Web Bluetooth. Utilisez une version Chromium avec Web Bluetooth activé, ou activez le flag de l'API si votre navigateur le masque.",
"insecure_context": "Web Bluetooth nécessite un contexte sécurisé (HTTPS ou localhost).",
"requires_https": "Ouvrez le flasher via HTTPS ou localhost pour activer le Bluetooth.",
"unknown": "Le Bluetooth ne peut pas être initialisé dans cet environnement.",
"android_bridge_no_web_bluetooth": "Permission Bluetooth accordée. Flashez via USB dans le flasher natif (Web Bluetooth n'est pas disponible dans la WebView).",
- "android_bluetooth_permission_required": "La permission Bluetooth est requise pour le BLE RNode maillé. Appuyez sur Autoriser le Bluetooth."
+ "android_bluetooth_permission_required": "La permission Bluetooth est requise pour le BLE RNode maillé. Appuyez sur Autoriser le Bluetooth.",
+ "brave_flag_disabled": "Brave désactive Web Bluetooth par défaut, donc le navigateur n'affiche jamais le sélecteur d'appareils.",
+ "brave_enable_flag": "Dans Brave, ouvrez brave://flags/#brave-web-bluetooth-api, réglez Web Bluetooth API sur Enabled, relancez, puis appuyez sur Revérifier.",
+ "brave_recheck": "Après activation du flag, utilisez Revérifier Bluetooth. L'appairage utilise le sélecteur requestDevice (pas de dialogue d'autorisation séparé comme pour le micro).",
+ "chromium_linux_hint": "Sous Linux, Chromium a besoin de BlueZ et d'une origine sécurisée (HTTPS ou localhost). Appuyez sur Tester Bluetooth pour ouvrir le sélecteur lorsque l'API est présente."
},
"actions": {
"load_polyfill": "Charger le polyfill",
@@ -2805,7 +2809,14 @@
"bluetooth_unsupported": "La demande de permission Bluetooth est indisponible.",
"bluetooth_granted": "Permission Bluetooth accordée.",
"bluetooth_denied": "Permission Bluetooth refusée.",
- "usb_requested": "Permission USB demandée."
+ "usb_requested": "Permission USB demandée.",
+ "probe_bluetooth": "Tester Bluetooth",
+ "recheck_capabilities": "Revérifier Bluetooth",
+ "bluetooth_now_available": "Web Bluetooth est disponible. Sélectionnez Bluetooth et continuez.",
+ "bluetooth_still_unavailable": "Web Bluetooth est toujours indisponible dans ce navigateur.",
+ "bluetooth_probe_ok": "Le sélecteur d'appareils Bluetooth a fonctionné. Le transport Bluetooth est prêt.",
+ "bluetooth_probe_cancelled": "Sélection d'appareil Bluetooth annulée.",
+ "bluetooth_probe_failed": "Échec du test Bluetooth : {error}"
}
},
"diagnostics": {
@@ -3342,7 +3353,9 @@
"ringtone_saved": "Sonnerie enregistrée avec succès",
"failed_save_ringtone": "Impossible d'enregistrer la sonnerie éditée",
"codec2_unavailable": "Codec2 is not available on this device. Low-bandwidth call profiles are hidden.",
- "codec2_profile_remapped": "Codec2 is unavailable so the call profile was switched to Opus."
+ "codec2_profile_remapped": "Codec2 is unavailable so the call profile was switched to Opus.",
+ "microphone_permission_needed": "Autorisez l'accès au microphone lorsque le navigateur le demande, puis cliquez à nouveau sur Actualiser les périphériques.",
+ "microphone_insecure_context": "L'accès au microphone nécessite HTTPS (ou localhost). Ouvrez MeshChatX via une URL sécurisée puis réessayez."
},
"contacts": {
"title": "Personnes-ressources",
diff --git a/meshchatx/src/frontend/locales/it.json b/meshchatx/src/frontend/locales/it.json
index 3624b0d6..8add349a 100644
--- a/meshchatx/src/frontend/locales/it.json
+++ b/meshchatx/src/frontend/locales/it.json
@@ -2834,12 +2834,16 @@
"bluetooth": {
"title": "Bluetooth non disponibile",
"android_bridge_not_implemented": "Web Bluetooth non è disponibile qui. Usa le impostazioni Bluetooth del sistema o un browser diverso.",
- "browser_unsupported": "Questo browser non supporta Web Bluetooth. Prova Chrome o Edge.",
+ "browser_unsupported": "Questo browser non espone Web Bluetooth. Usa una build Chromium con Web Bluetooth abilitato, oppure attiva il flag dell'API se il browser lo nasconde.",
"insecure_context": "Web Bluetooth richiede un contesto sicuro (HTTPS o localhost).",
"requires_https": "Apri il flasher tramite HTTPS o localhost per abilitare il Bluetooth.",
"unknown": "Bluetooth non può essere inizializzato in questo ambiente.",
"android_bridge_no_web_bluetooth": "Autorizzazione Bluetooth concessa. Flasha via USB nel flasher nativo (Web Bluetooth non è disponibile in WebView).",
- "android_bluetooth_permission_required": "È richiesta l'autorizzazione Bluetooth per RNode BLE mesh. Tocca Consenti Bluetooth."
+ "android_bluetooth_permission_required": "È richiesta l'autorizzazione Bluetooth per RNode BLE mesh. Tocca Consenti Bluetooth.",
+ "brave_flag_disabled": "Brave disabilita Web Bluetooth per impostazione predefinita, quindi il browser non mostra mai il selettore dispositivi.",
+ "brave_enable_flag": "In Brave apri brave://flags/#brave-web-bluetooth-api, imposta Web Bluetooth API su Enabled, riavvia e poi tocca Ricontrolla.",
+ "brave_recheck": "Dopo aver abilitato il flag, usa Ricontrolla Bluetooth. L'associazione usa il selettore di requestDevice (non c'è un dialogo di permesso separato come per il microfono).",
+ "chromium_linux_hint": "Su Linux Chromium richiede BlueZ funzionante e un'origine sicura (HTTPS o localhost). Tocca Prova Bluetooth per aprire il selettore quando l'API è presente."
},
"actions": {
"load_polyfill": "Carica polyfill",
@@ -2857,7 +2861,14 @@
"bluetooth_unsupported": "La richiesta di autorizzazione Bluetooth non è disponibile.",
"bluetooth_granted": "Autorizzazione Bluetooth concessa.",
"bluetooth_denied": "Autorizzazione Bluetooth negata.",
- "usb_requested": "Autorizzazione USB richiesta."
+ "usb_requested": "Autorizzazione USB richiesta.",
+ "probe_bluetooth": "Prova Bluetooth",
+ "recheck_capabilities": "Ricontrolla Bluetooth",
+ "bluetooth_now_available": "Web Bluetooth è disponibile. Seleziona Bluetooth e continua.",
+ "bluetooth_still_unavailable": "Web Bluetooth non è ancora disponibile in questo browser.",
+ "bluetooth_probe_ok": "Il selettore dispositivi Bluetooth ha funzionato. Il trasporto Bluetooth è pronto.",
+ "bluetooth_probe_cancelled": "Selezione dispositivo Bluetooth annullata.",
+ "bluetooth_probe_failed": "Test Bluetooth non riuscito: {error}"
}
},
"diagnostics": {
@@ -3394,7 +3405,9 @@
"ringtone_saved": "Suoneria salvata con successo",
"failed_save_ringtone": "Impossibile salvare la suoneria modificata",
"codec2_unavailable": "Codec2 is not available on this device. Low-bandwidth call profiles are hidden.",
- "codec2_profile_remapped": "Codec2 is unavailable so the call profile was switched to Opus."
+ "codec2_profile_remapped": "Codec2 is unavailable so the call profile was switched to Opus.",
+ "microphone_permission_needed": "Consenti l'accesso al microfono quando il browser lo richiede, poi fai di nuovo clic su Aggiorna dispositivi.",
+ "microphone_insecure_context": "L'accesso al microfono richiede HTTPS (o localhost). Apri MeshChatX con un URL sicuro e riprova."
},
"tutorial": {
"title": "Guida Introduttiva",
diff --git a/meshchatx/src/frontend/locales/nl.json b/meshchatx/src/frontend/locales/nl.json
index e8173030..43a176a9 100644
--- a/meshchatx/src/frontend/locales/nl.json
+++ b/meshchatx/src/frontend/locales/nl.json
@@ -2782,12 +2782,16 @@
"bluetooth": {
"title": "Bluetooth is niet beschikbaar",
"android_bridge_not_implemented": "Web Bluetooth is niet beschikbaar hier. Gebruik de Bluetooth-instellingen van het systeem of een andere browser.",
- "browser_unsupported": "Deze browser ondersteunt Web Bluetooth niet. Probeer Chrome of Edge.",
+ "browser_unsupported": "Deze browser biedt Web Bluetooth niet. Gebruik een Chromium-build met Web Bluetooth ingeschakeld, of schakel de API-vlag in als je browser die verbergt.",
"insecure_context": "Web Bluetooth vereist een beveiligde context (HTTPS of localhost).",
"requires_https": "Open de flasher via HTTPS of localhost om Bluetooth in te schakelen.",
"unknown": "Bluetooth kan niet worden geïnitialiseerd in deze omgeving.",
"android_bridge_no_web_bluetooth": "Bluetooth-toestemming verleend. Flash via USB in de native flasher (Web Bluetooth is niet beschikbaar in WebView).",
- "android_bluetooth_permission_required": "Bluetooth-toestemming is vereist voor mesh RNode BLE. Tik op Bluetooth toestaan."
+ "android_bluetooth_permission_required": "Bluetooth-toestemming is vereist voor mesh RNode BLE. Tik op Bluetooth toestaan.",
+ "brave_flag_disabled": "Brave schakelt Web Bluetooth standaard uit, dus de browser toont nooit een apparaatkiezer.",
+ "brave_enable_flag": "Open in Brave brave://flags/#brave-web-bluetooth-api, zet Web Bluetooth API op Enabled, herstart en tik op Opnieuw controleren.",
+ "brave_recheck": "Na het inschakelen van de vlag gebruik je Opnieuw controleren. Koppelen gebruikt de apparaatkiezer van requestDevice (er is geen aparte toestemmingsdialoog zoals bij de microfoon).",
+ "chromium_linux_hint": "Op Linux heeft Chromium werkende BlueZ en een beveiligde herkomst nodig (HTTPS of localhost). Tik op Bluetooth proberen om de kiezer te openen wanneer de API aanwezig is."
},
"actions": {
"load_polyfill": "Polyfill laden",
@@ -2805,7 +2809,14 @@
"bluetooth_unsupported": "Bluetooth-toestemmingsverzoek is niet beschikbaar.",
"bluetooth_granted": "Bluetooth-toestemming verleend.",
"bluetooth_denied": "Bluetooth-toestemming geweigerd.",
- "usb_requested": "USB-toestemming aangevraagd."
+ "usb_requested": "USB-toestemming aangevraagd.",
+ "probe_bluetooth": "Bluetooth proberen",
+ "recheck_capabilities": "Bluetooth opnieuw controleren",
+ "bluetooth_now_available": "Web Bluetooth is beschikbaar. Selecteer Bluetooth en ga verder.",
+ "bluetooth_still_unavailable": "Web Bluetooth is in deze browser nog steeds niet beschikbaar.",
+ "bluetooth_probe_ok": "Bluetooth-apparaatkiezer werkte. Bluetooth-transport is gereed.",
+ "bluetooth_probe_cancelled": "Bluetooth-apparaatselectie geannuleerd.",
+ "bluetooth_probe_failed": "Bluetooth-test mislukt: {error}"
}
},
"diagnostics": {
@@ -3342,7 +3353,9 @@
"ringtone_saved": "Ringtone is succesvol opgeslagen",
"failed_save_ringtone": "Opslaan van bewerkte ringtone mislukt",
"codec2_unavailable": "Codec2 is not available on this device. Low-bandwidth call profiles are hidden.",
- "codec2_profile_remapped": "Codec2 is unavailable so the call profile was switched to Opus."
+ "codec2_profile_remapped": "Codec2 is unavailable so the call profile was switched to Opus.",
+ "microphone_permission_needed": "Sta microfoontoegang toe wanneer de browser daarom vraagt en klik daarna opnieuw op Apparaten vernieuwen.",
+ "microphone_insecure_context": "Microfoontoegang vereist HTTPS (of localhost). Open MeshChatX via een beveiligde URL en probeer opnieuw."
},
"contacts": {
"title": "Contacten",
diff --git a/meshchatx/src/frontend/locales/ru.json b/meshchatx/src/frontend/locales/ru.json
index 30cc28e5..dd489456 100644
--- a/meshchatx/src/frontend/locales/ru.json
+++ b/meshchatx/src/frontend/locales/ru.json
@@ -2576,12 +2576,16 @@
"bluetooth": {
"title": "Bluetooth недоступен",
"android_bridge_not_implemented": "Web Bluetooth здесь недоступен. Используйте настройки Bluetooth в ОС или другой браузер.",
- "browser_unsupported": "Этот браузер не поддерживает Web Bluetooth. Попробуйте Chrome или Edge.",
+ "browser_unsupported": "Этот браузер не предоставляет Web Bluetooth. Используйте сборку Chromium с включённым Web Bluetooth или включите флаг API, если браузер его скрывает.",
"insecure_context": "Web Bluetooth требует безопасного контекста (HTTPS или localhost).",
"requires_https": "Откройте прошивальщик через HTTPS или localhost, чтобы включить Bluetooth.",
"unknown": "Bluetooth не может быть инициализирован в этом окружении.",
"android_bridge_no_web_bluetooth": "Разрешение Bluetooth выдано. Прошивайте по USB во встроенном прошивальщике (Web Bluetooth недоступен в WebView).",
- "android_bluetooth_permission_required": "Для mesh RNode BLE требуется разрешение Bluetooth. Нажмите «Разрешить Bluetooth»."
+ "android_bluetooth_permission_required": "Для mesh RNode BLE требуется разрешение Bluetooth. Нажмите «Разрешить Bluetooth».",
+ "brave_flag_disabled": "Brave по умолчанию отключает Web Bluetooth, поэтому браузер никогда не показывает выбор устройства.",
+ "brave_enable_flag": "В Brave откройте brave://flags/#brave-web-bluetooth-api, установите Web Bluetooth API в Enabled, перезапустите и нажмите Проверить снова.",
+ "brave_recheck": "После включения флага используйте Проверить Bluetooth снова. Сопряжение использует выбор устройства requestDevice (отдельного диалога разрешения, как у микрофона, нет).",
+ "chromium_linux_hint": "В Linux Chromium нужен рабочий BlueZ и безопасный origin (HTTPS или localhost). Нажмите Проверить Bluetooth, чтобы открыть выбор устройства, когда API доступен."
},
"actions": {
"load_polyfill": "Загрузить полифилл",
@@ -2599,7 +2603,14 @@
"bluetooth_unsupported": "Запрос разрешения Bluetooth недоступен.",
"bluetooth_granted": "Разрешение Bluetooth выдано.",
"bluetooth_denied": "В разрешении Bluetooth отказано.",
- "usb_requested": "Запрошено разрешение USB."
+ "usb_requested": "Запрошено разрешение USB.",
+ "probe_bluetooth": "Проверить Bluetooth",
+ "recheck_capabilities": "Проверить Bluetooth снова",
+ "bluetooth_now_available": "Web Bluetooth доступен. Выберите Bluetooth и продолжите.",
+ "bluetooth_still_unavailable": "Web Bluetooth по-прежнему недоступен в этом браузере.",
+ "bluetooth_probe_ok": "Выбор Bluetooth-устройства сработал. Bluetooth-транспорт готов.",
+ "bluetooth_probe_cancelled": "Выбор Bluetooth-устройства отменён.",
+ "bluetooth_probe_failed": "Проверка Bluetooth не удалась: {error}"
}
},
"diagnostics": {
@@ -3136,7 +3147,9 @@
"ringtone_saved": "Рингтон успешно сохранён",
"failed_save_ringtone": "Не удалось сохранить отредактированный рингтон",
"codec2_unavailable": "Codec2 is not available on this device. Low-bandwidth call profiles are hidden.",
- "codec2_profile_remapped": "Codec2 is unavailable so the call profile was switched to Opus."
+ "codec2_profile_remapped": "Codec2 is unavailable so the call profile was switched to Opus.",
+ "microphone_permission_needed": "Разрешите доступ к микрофону, когда браузер запросит его, затем снова нажмите Обновить устройства.",
+ "microphone_insecure_context": "Доступ к микрофону требует HTTPS (или localhost). Откройте MeshChatX по защищённому URL и попробуйте снова."
},
"tutorial": {
"title": "Начало работы",
diff --git a/meshchatx/src/frontend/locales/zh.json b/meshchatx/src/frontend/locales/zh.json
index ae94eb3a..015d9ec5 100644
--- a/meshchatx/src/frontend/locales/zh.json
+++ b/meshchatx/src/frontend/locales/zh.json
@@ -2782,12 +2782,16 @@
"bluetooth": {
"title": "蓝牙不可用",
"android_bridge_not_implemented": "此处的 Web 蓝牙不可用。请使用系统蓝牙设置或不同的浏览器。",
- "browser_unsupported": "此浏览器不支持 Web Bluetooth。请尝试 Chrome 或 Edge。",
+ "browser_unsupported": "此浏览器未提供 Web Bluetooth。请使用已启用 Web Bluetooth 的 Chromium 构建,或在浏览器隐藏该 API 时启用相应标志。",
"insecure_context": "Web Bluetooth 需要安全上下文(HTTPS 或 localhost)。",
"requires_https": "请通过 HTTPS 或 localhost 打开刷写工具以启用蓝牙。",
"unknown": "在此环境中无法初始化蓝牙。",
"android_bridge_no_web_bluetooth": "已授予蓝牙权限。请在原生刷写工具中通过 USB 刷写(WebView 中不可用 Web Bluetooth)。",
- "android_bluetooth_permission_required": "网格 RNode BLE 需要蓝牙权限。请点按“允许蓝牙”。"
+ "android_bluetooth_permission_required": "网格 RNode BLE 需要蓝牙权限。请点按“允许蓝牙”。",
+ "brave_flag_disabled": "Brave 默认禁用 Web Bluetooth,因此浏览器永远不会显示设备选择提示。",
+ "brave_enable_flag": "在 Brave 中打开 brave://flags/#brave-web-bluetooth-api,将 Web Bluetooth API 设为 Enabled,重新启动后点击重新检查。",
+ "brave_recheck": "启用标志后,使用重新检查蓝牙。配对使用 requestDevice 的设备选择器(没有像麦克风那样的单独权限对话框)。",
+ "chromium_linux_hint": "在 Linux 上,Chromium 需要可用的 BlueZ 以及安全源(HTTPS 或 localhost)。当 API 可用时,点击尝试蓝牙以打开设备选择器。"
},
"actions": {
"load_polyfill": "加载 polyfill",
@@ -2805,7 +2809,14 @@
"bluetooth_unsupported": "无法请求蓝牙权限。",
"bluetooth_granted": "已授予蓝牙权限。",
"bluetooth_denied": "蓝牙权限被拒绝。",
- "usb_requested": "已请求 USB 权限。"
+ "usb_requested": "已请求 USB 权限。",
+ "probe_bluetooth": "尝试蓝牙",
+ "recheck_capabilities": "重新检查蓝牙",
+ "bluetooth_now_available": "Web Bluetooth 可用。请选择蓝牙并继续。",
+ "bluetooth_still_unavailable": "此浏览器中 Web Bluetooth 仍不可用。",
+ "bluetooth_probe_ok": "蓝牙设备选择器工作正常。蓝牙传输已就绪。",
+ "bluetooth_probe_cancelled": "已取消蓝牙设备选择。",
+ "bluetooth_probe_failed": "蓝牙检测失败:{error}"
}
},
"diagnostics": {
@@ -3342,7 +3353,9 @@
"ringtone_saved": "铃声保存成功",
"failed_save_ringtone": "保存编辑的铃声失败",
"codec2_unavailable": "Codec2 is not available on this device. Low-bandwidth call profiles are hidden.",
- "codec2_profile_remapped": "Codec2 is unavailable so the call profile was switched to Opus."
+ "codec2_profile_remapped": "Codec2 is unavailable so the call profile was switched to Opus.",
+ "microphone_permission_needed": "请在浏览器提示时允许麦克风访问,然后再次点击刷新设备。",
+ "microphone_insecure_context": "麦克风访问需要 HTTPS(或 localhost)。请通过安全 URL 打开 MeshChatX 后重试。"
},
"contacts": {
"title": "联系人",
diff --git a/tests/backend/test_csp_logic.py b/tests/backend/test_csp_logic.py
index a20b8ad8..01cbe449 100644
--- a/tests/backend/test_csp_logic.py
+++ b/tests/backend/test_csp_logic.py
@@ -70,7 +70,7 @@ async def test_csp_header_logic(mock_rns_minimal, tmp_path):
assert "wasm-unsafe-eval" in csp
assert (
response.headers.get("Permissions-Policy")
- == "microphone=(self), camera=(self)"
+ == "microphone=(self), camera=(self), bluetooth=(self), serial=(self), usb=(self)"
)
m = re.search(r"script-src([^;]+);", csp)
assert m is not None and "blob:" in m.group(1)
diff --git a/tests/e2e/browser-permissions.spec.js b/tests/e2e/browser-permissions.spec.js
new file mode 100644
index 00000000..850dfdc8
--- /dev/null
+++ b/tests/e2e/browser-permissions.spec.js
@@ -0,0 +1,218 @@
+// SPDX-License-Identifier: 0BSD
+
+const { test, expect } = require("@playwright/test");
+const { E2E_BACKEND_ORIGIN, prepareE2eSession } = require("./helpers");
+
+/**
+ * Browser permission / chooser regressions for Calls mic and RNode Bluetooth.
+ * Real OS prompts cannot be driven reliably in CI, so these tests install
+ * controlled navigator.mediaDevices / navigator.bluetooth shims and assert the
+ * product code asks the browser the right way.
+ */
+
+async function installMicProbe(page, { secure = true } = {}) {
+ await page.addInitScript((isSecure) => {
+ Object.defineProperty(window, "isSecureContext", {
+ configurable: true,
+ get() {
+ return isSecure;
+ },
+ });
+ window.__meshchatxGumCalls = [];
+ const stop = () => {};
+ const fakeStream = {
+ getTracks() {
+ return [{ stop, kind: "audio", stopTrack: stop }];
+ },
+ };
+ const getUserMedia = async (constraints) => {
+ window.__meshchatxGumCalls.push(constraints);
+ return fakeStream;
+ };
+ const enumerateDevices = async () => [
+ { kind: "audioinput", deviceId: "mic-1", label: "Fake Mic", groupId: "g1" },
+ { kind: "audiooutput", deviceId: "spk-1", label: "Fake Speaker", groupId: "g1" },
+ ];
+ Object.defineProperty(navigator, "mediaDevices", {
+ configurable: true,
+ value: { getUserMedia, enumerateDevices },
+ });
+ }, secure);
+}
+
+async function enableWebAudioBridge(page) {
+ await page.goto("/#/call");
+ await expect(page.getByRole("button", { name: "Phone", exact: true })).toBeVisible({ timeout: 30000 });
+ const refresh = page.getByRole("button", { name: "Refresh Devices", exact: true });
+ if (
+ (await refresh.count()) === 0 ||
+ !(await refresh
+ .first()
+ .isVisible()
+ .catch(() => false))
+ ) {
+ const label = page.locator('label[for="web-audio-toggle"]');
+ await expect(label).toBeVisible({ timeout: 20000 });
+ const input = page.locator("#web-audio-toggle");
+ const checked = await input.isChecked().catch(() => false);
+ const disabled = await input.isDisabled().catch(() => false);
+ if (!checked && !disabled) {
+ await label.click();
+ }
+ }
+ await expect(refresh).toBeVisible({ timeout: 15000 });
+}
+
+async function dismissViteOverlay(page) {
+ await page.evaluate(() => {
+ document.querySelectorAll("vite-error-overlay").forEach((el) => el.remove());
+ });
+}
+
+test.describe("Browser mic permission prompt path", () => {
+ test.beforeEach(async ({ request }) => {
+ await prepareE2eSession(request);
+ });
+
+ test("Refresh Devices prompts getUserMedia with bare audio first", async ({ page }) => {
+ await installMicProbe(page, { secure: true });
+ await enableWebAudioBridge(page);
+ await page.evaluate(() => {
+ window.__meshchatxGumCalls = [];
+ });
+ await page.getByRole("button", { name: "Refresh Devices", exact: true }).click();
+ await expect.poll(async () => page.evaluate(() => window.__meshchatxGumCalls.length)).toBeGreaterThan(0);
+ const calls = await page.evaluate(() => window.__meshchatxGumCalls);
+ expect(calls[0]).toEqual({ audio: true });
+ });
+
+ test("Refresh Devices refuses insecure contexts without calling getUserMedia", async ({ page }) => {
+ await installMicProbe(page, { secure: false });
+ await enableWebAudioBridge(page);
+ await page.evaluate(() => {
+ window.__meshchatxGumCalls = [];
+ });
+ await page.getByRole("button", { name: "Refresh Devices", exact: true }).click();
+ await expect(page.getByText(/Microphone access needs HTTPS|secure URL/i).first()).toBeVisible({
+ timeout: 10000,
+ });
+ const calls = await page.evaluate(() => window.__meshchatxGumCalls);
+ expect(calls).toEqual([]);
+ });
+});
+
+test.describe("RNode flasher Web Bluetooth chooser path", () => {
+ test.beforeEach(async ({ request }) => {
+ await prepareE2eSession(request);
+ });
+
+ test("Brave without Web Bluetooth shows Try Bluetooth and flag guidance", async ({ page }) => {
+ await page.addInitScript(() => {
+ Object.defineProperty(window, "isSecureContext", {
+ configurable: true,
+ get() {
+ return true;
+ },
+ });
+ Object.defineProperty(navigator, "userAgent", {
+ configurable: true,
+ get() {
+ return "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Brave/1.70 Chrome/120.0.0.0 Safari/537.36";
+ },
+ });
+ Object.defineProperty(navigator, "brave", {
+ configurable: true,
+ value: { isBrave: async () => true },
+ });
+ try {
+ delete navigator.bluetooth;
+ } catch {
+ Object.defineProperty(navigator, "bluetooth", {
+ configurable: true,
+ value: undefined,
+ });
+ }
+ });
+ await page.goto("/#/tools/rnode-flasher");
+ await expect(page.getByText("Bluetooth is unavailable").first()).toBeVisible({ timeout: 30000 });
+ await expect(page.getByText(/brave:\/\/flags\/#brave-web-bluetooth-api/i).first()).toBeVisible();
+ await dismissViteOverlay(page);
+ const tryBtn = page.getByRole("button", { name: /Try Bluetooth/i });
+ const recheckBtn = page.getByRole("button", { name: /Recheck Bluetooth/i });
+ await expect(tryBtn).toBeVisible();
+ await expect(recheckBtn).toBeVisible();
+ await tryBtn.click({ force: true });
+ await expect(page.getByText(/Brave disables Web Bluetooth|brave:\/\/flags/i).first()).toBeVisible({
+ timeout: 10000,
+ });
+ });
+
+ test("Try Bluetooth calls requestDevice when Web Bluetooth becomes available", async ({ page }) => {
+ await page.addInitScript(() => {
+ Object.defineProperty(window, "isSecureContext", {
+ configurable: true,
+ get() {
+ return true;
+ },
+ });
+ Object.defineProperty(navigator, "userAgent", {
+ configurable: true,
+ get() {
+ return "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Brave/1.70 Chrome/120.0.0.0 Safari/537.36";
+ },
+ });
+ Object.defineProperty(navigator, "brave", {
+ configurable: true,
+ value: { isBrave: async () => true },
+ });
+ try {
+ delete navigator.bluetooth;
+ } catch {
+ Object.defineProperty(navigator, "bluetooth", {
+ configurable: true,
+ value: undefined,
+ });
+ }
+ window.__meshchatxBtRequests = 0;
+ window.__meshchatxInstallBluetooth = () => {
+ Object.defineProperty(navigator, "bluetooth", {
+ configurable: true,
+ value: {
+ requestDevice: async () => {
+ window.__meshchatxBtRequests += 1;
+ return {
+ gatt: {
+ connected: false,
+ disconnect() {},
+ },
+ };
+ },
+ getAvailability: async () => true,
+ },
+ });
+ };
+ });
+ await page.goto("/#/tools/rnode-flasher");
+ const tryBtn = page.getByRole("button", { name: /Try Bluetooth/i });
+ await expect(tryBtn).toBeVisible({ timeout: 30000 });
+ await dismissViteOverlay(page);
+ await page.evaluate(() => window.__meshchatxInstallBluetooth());
+ await tryBtn.click({ force: true });
+ await expect.poll(async () => page.evaluate(() => window.__meshchatxBtRequests)).toBeGreaterThan(0);
+ await expect(
+ page.getByText(/Bluetooth device chooser worked|Bluetooth transport is ready/i).first()
+ ).toBeVisible({
+ timeout: 10000,
+ });
+ });
+
+ test("Permissions-Policy from backend allows bluetooth for flasher origin", async ({ request }) => {
+ const index = await request.get(`${E2E_BACKEND_ORIGIN}/`);
+ expect(index.ok()).toBeTruthy();
+ const policy = index.headers()["permissions-policy"] || "";
+ expect(policy).toContain("bluetooth=(self)");
+ expect(policy).toContain("microphone=(self)");
+ expect(policy).toContain("serial=(self)");
+ expect(policy).toContain("usb=(self)");
+ });
+});
diff --git a/tests/frontend/CallPage.test.js b/tests/frontend/CallPage.test.js
index 63e56813..2de85ca1 100644
--- a/tests/frontend/CallPage.test.js
+++ b/tests/frontend/CallPage.test.js
@@ -421,7 +421,7 @@ describe("CallPage.vue", () => {
expect(stream).toBe(fakeStream);
});
- it("requestAudioPermission prompts getUserMedia even when enumerate lists only speakers", async () => {
+ it("requestAudioPermission prompts getUserMedia with bare audio first", async () => {
const wrapper = mountCallPage();
await flushPromises();
const stop = vi.fn();
@@ -437,13 +437,7 @@ describe("CallPage.vue", () => {
try {
await expect(wrapper.vm.requestAudioPermission()).resolves.toBe(true);
expect(getUserMedia).toHaveBeenCalledTimes(1);
- expect(getUserMedia.mock.calls[0][0]).toEqual({
- audio: {
- echoCancellation: true,
- noiseSuppression: true,
- autoGainControl: true,
- },
- });
+ expect(getUserMedia.mock.calls[0][0]).toEqual({ audio: true });
expect(stop).toHaveBeenCalled();
expect(enumerateDevices).toHaveBeenCalled();
} finally {
@@ -455,6 +449,82 @@ describe("CallPage.vue", () => {
}
});
+ it("requestAudioPermission retries processing constraints after NotFoundError on bare audio", async () => {
+ const wrapper = mountCallPage();
+ await flushPromises();
+ const stop = vi.fn();
+ const notFound = new Error("missing");
+ notFound.name = "NotFoundError";
+ const getUserMedia = vi
+ .fn()
+ .mockRejectedValueOnce(notFound)
+ .mockResolvedValueOnce({ getTracks: () => [{ stop }] });
+ Object.defineProperty(navigator, "mediaDevices", {
+ configurable: true,
+ value: {
+ getUserMedia,
+ enumerateDevices: vi.fn().mockResolvedValue([]),
+ },
+ });
+ await expect(wrapper.vm.requestAudioPermission()).resolves.toBe(true);
+ expect(getUserMedia).toHaveBeenCalledTimes(2);
+ expect(getUserMedia.mock.calls[0][0]).toEqual({ audio: true });
+ expect(getUserMedia.mock.calls[1][0]).toEqual({
+ audio: {
+ echoCancellation: true,
+ noiseSuppression: true,
+ autoGainControl: true,
+ },
+ });
+ expect(stop).toHaveBeenCalled();
+ });
+
+ it("requestAudioPermission refuses insecure contexts without calling getUserMedia", async () => {
+ const wrapper = mountCallPage();
+ await flushPromises();
+ const getUserMedia = vi.fn();
+ Object.defineProperty(navigator, "mediaDevices", {
+ configurable: true,
+ value: { getUserMedia, enumerateDevices: vi.fn() },
+ });
+ const secureDesc = Object.getOwnPropertyDescriptor(window, "isSecureContext");
+ Object.defineProperty(window, "isSecureContext", {
+ configurable: true,
+ value: false,
+ });
+ try {
+ await expect(wrapper.vm.requestAudioPermission()).resolves.toBe(false);
+ expect(getUserMedia).not.toHaveBeenCalled();
+ } finally {
+ if (secureDesc) {
+ Object.defineProperty(window, "isSecureContext", secureDesc);
+ } else {
+ Reflect.deleteProperty(window, "isSecureContext");
+ }
+ }
+ });
+
+ it("resolveMissingMicErrorKey prefers permission-needed when prompt is pending", async () => {
+ const wrapper = mountCallPage();
+ await flushPromises();
+ const permsDesc = Object.getOwnPropertyDescriptor(navigator, "permissions");
+ Object.defineProperty(navigator, "permissions", {
+ configurable: true,
+ value: {
+ query: vi.fn().mockResolvedValue({ state: "prompt" }),
+ },
+ });
+ try {
+ await expect(wrapper.vm.resolveMissingMicErrorKey()).resolves.toBe("call.microphone_permission_needed");
+ } finally {
+ if (permsDesc) {
+ Object.defineProperty(navigator, "permissions", permsDesc);
+ } else {
+ Reflect.deleteProperty(navigator, "permissions");
+ }
+ }
+ });
+
it("pickWebAudioMicConstraints includes browser audio processing hints", async () => {
const wrapper = mountCallPage();
await flushPromises();
@@ -469,6 +539,16 @@ describe("CallPage.vue", () => {
expect(constraints.audio.deviceId).toEqual({ exact: "mic-1" });
});
+ it("pickWebAudioMicConstraints uses bare audio for Default selection", async () => {
+ const wrapper = mountCallPage();
+ await flushPromises();
+ wrapper.vm.selectedAudioInputId = "__meshchat_default_in__";
+ const constraints = wrapper.vm.pickWebAudioMicConstraints({
+ enumerateDevices: vi.fn().mockResolvedValue([]),
+ });
+ expect(constraints).toEqual({ audio: true });
+ });
+
it("startWebAudio uses MeshChatXAndroid native bridge when platform is android", async () => {
const wrapper = mountCallPage();
await flushPromises();
diff --git a/tests/frontend/RNodeCapabilities.test.js b/tests/frontend/RNodeCapabilities.test.js
index 57794533..650b2ffb 100644
--- a/tests/frontend/RNodeCapabilities.test.js
+++ b/tests/frontend/RNodeCapabilities.test.js
@@ -87,6 +87,36 @@ describe("Capabilities.detectCapabilities", () => {
expect(caps.transports[TRANSPORT_BLUETOOTH].reason).toBe("insecure_context");
});
+ it("detects Brave with Web Bluetooth disabled as brave_flag_disabled", () => {
+ const env = mkEnv({
+ isSecureContext: true,
+ navigator: { userAgent: "Mozilla/5.0 Brave/1.0" },
+ });
+ const caps = detectCapabilities({ env });
+ expect(caps.platform.isBrave).toBe(true);
+ expect(caps.transports[TRANSPORT_BLUETOOTH].available).toBe(false);
+ expect(caps.transports[TRANSPORT_BLUETOOTH].reason).toBe("brave_flag_disabled");
+ });
+
+ it("detects Brave via navigator.brave even without Brave in UA", () => {
+ const env = mkEnv({
+ isSecureContext: true,
+ navigator: { userAgent: "Mozilla/5.0 Chrome/120", brave: {} },
+ });
+ const caps = detectCapabilities({ env });
+ expect(caps.platform.isBrave).toBe(true);
+ expect(caps.transports[TRANSPORT_BLUETOOTH].reason).toBe("brave_flag_disabled");
+ });
+
+ it("prefers insecure_context over brave_flag_disabled", () => {
+ const env = mkEnv({
+ isSecureContext: false,
+ navigator: { userAgent: "Mozilla/5.0 Brave/1.0" },
+ });
+ const caps = detectCapabilities({ env });
+ expect(caps.transports[TRANSPORT_BLUETOOTH].reason).toBe("insecure_context");
+ });
+
it("always exposes wifi transport as available", () => {
const env = mkEnv();
const caps = detectCapabilities({ env });
@@ -128,4 +158,12 @@ describe("Capabilities.transportSuggestionKeys", () => {
expect(keys).toContain("tools.rnode_flasher.support.bluetooth.insecure_context");
expect(keys).toContain("tools.rnode_flasher.support.bluetooth.requires_https");
});
+ it("includes Brave flag guidance when Web Bluetooth is disabled", () => {
+ const env = mkEnv({ navigator: { userAgent: "Mozilla/5.0 Brave/1.0" } });
+ const caps = detectCapabilities({ env });
+ const keys = transportSuggestionKeys(caps, TRANSPORT_BLUETOOTH);
+ expect(keys).toContain("tools.rnode_flasher.support.bluetooth.brave_flag_disabled");
+ expect(keys).toContain("tools.rnode_flasher.support.bluetooth.brave_enable_flag");
+ expect(keys).toContain("tools.rnode_flasher.support.bluetooth.brave_recheck");
+ });
});
diff --git a/tests/frontend/RNodeComponents.test.js b/tests/frontend/RNodeComponents.test.js
index 9d211775..aa0a175b 100644
--- a/tests/frontend/RNodeComponents.test.js
+++ b/tests/frontend/RNodeComponents.test.js
@@ -72,6 +72,19 @@ describe("RNodeCapabilitiesBanner", () => {
expect(labels.some((l) => l.includes("request_bluetooth"))).toBe(true);
expect(labels.some((l) => l.includes("open_settings"))).toBe(true);
});
+
+ it("shows desktop Try Bluetooth and Recheck actions when Web Bluetooth is missing", () => {
+ const env = { isSecureContext: true, navigator: { userAgent: "Mozilla/5.0 Brave/1.0" } };
+ const caps = detectCapabilities({ env });
+ const wrapper = mountWith(RNodeCapabilitiesBanner, {
+ capabilities: caps,
+ androidAvailable: false,
+ });
+ expect(caps.transports.bluetooth.reason).toBe("brave_flag_disabled");
+ const labels = wrapper.findAll("button").map((b) => b.text());
+ expect(labels.some((l) => l.includes("probe_bluetooth"))).toBe(true);
+ expect(labels.some((l) => l.includes("recheck_capabilities"))).toBe(true);
+ });
});
describe("RNodeDeviceSelector", () => {
diff --git a/tests/frontend/behaviorContracts.test.js b/tests/frontend/behaviorContracts.test.js
index 64d087e1..81e22f2f 100644
--- a/tests/frontend/behaviorContracts.test.js
+++ b/tests/frontend/behaviorContracts.test.js
@@ -450,7 +450,8 @@ describe("behavior contracts: locale, theme, and call audio", () => {
it("CallPage refresh devices uses getUserMedia before device enumeration", () => {
const call = readSource("meshchatx/src/frontend/components/call/CallPage.vue");
- expect(call).toContain("Do not gate on enumerateDevices before getUserMedia");
+ expect(call).toContain("Wide-open { audio: true } is what");
+ expect(call).toMatch(/requestAudioPermission[\s\S]*getUserMedia\(\{\s*audio:\s*true\s*\}/);
expect(call).toMatch(/requestAudioPermission[\s\S]*getUserMedia[\s\S]*refreshAudioDevices/);
});
});
diff --git a/tests/frontend/localeTheme.adversarial.test.js b/tests/frontend/localeTheme.adversarial.test.js
index b4c62296..17ef35d5 100644
--- a/tests/frontend/localeTheme.adversarial.test.js
+++ b/tests/frontend/localeTheme.adversarial.test.js
@@ -132,6 +132,7 @@ describe("localeTheme adversarial / fuzz", () => {
const beforeGum = body.slice(0, gum);
expect(beforeGum).not.toMatch(/\.enumerateDevices\s*\(/);
expect(beforeGum).not.toContain("no_audio_input_found");
+ expect(body).toMatch(/getUserMedia\(\{\s*audio:\s*true\s*\}/);
});
it("contract: localeLoader setLocale normalizes before loading messages", () => {
@@ -149,5 +150,8 @@ describe("localeTheme adversarial / fuzz", () => {
const mw = readSource("meshchatx/src/backend/http/middleware.py");
expect(mw).toContain("Permissions-Policy");
expect(mw).toContain("microphone=(self)");
+ expect(mw).toContain("bluetooth=(self)");
+ expect(mw).toContain("serial=(self)");
+ expect(mw).toContain("usb=(self)");
});
});
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────